from typing import Optional

from pydoll.protocol.base import Command
from pydoll.protocol.input.methods import (
    CancelDraggingCommand,
    DispatchDragEventCommand,
    DispatchDragEventParams,
    DispatchKeyEventCommand,
    DispatchKeyEventParams,
    DispatchMouseEventCommand,
    DispatchMouseEventParams,
    DispatchTouchEventCommand,
    DispatchTouchEventParams,
    DragData,
    EmulateTouchFromMouseEventCommand,
    EmulateTouchFromMouseEventParams,
    ImeSetCompositionCommand,
    ImeSetCompositionParams,
    InputMethod,
    InsertTextCommand,
    InsertTextParams,
    SetIgnoreInputEventsCommand,
    SetIgnoreInputEventsParams,
    SetInterceptDragsCommand,
    SetInterceptDragsParams,
    SynthesizePinchGestureCommand,
    SynthesizePinchGestureParams,
    SynthesizeScrollGestureCommand,
    SynthesizeScrollGestureParams,
    SynthesizeTapGestureCommand,
    SynthesizeTapGestureParams,
    TouchPoint,
)
from pydoll.protocol.input.types import (
    DragEventType,
    GestureSourceType,
    KeyEventType,
    KeyLocation,
    KeyModifier,
    MouseButton,
    MouseEventType,
    PointerType,
    TouchEventType,
)


class InputCommands:
    """
    A class for simulating user input events using Chrome DevTools Protocol.

    The Input domain provides methods for simulating user input, including:
    - Keyboard events (key presses, releases)
    - Mouse events (clicks, movements, wheel)
    - Touch events (taps, multi-touch gestures)
    - Drag and drop events
    - Synthetic gestures (pinch, scroll, tap)

    These methods allow for programmatic control of input events without requiring
    actual user interaction, making it useful for testing and automation.
    """

    @staticmethod
    def cancel_dragging() -> CancelDraggingCommand:
        """
        Generates a command to cancel any active dragging in the page.

        This is useful when you need to interrupt an ongoing drag operation
        that might have been started with dispatchDragEvent or by other means.

        Returns:
            Command: The CDP command to cancel dragging.
        """
        return Command(method=InputMethod.CANCEL_DRAGGING)

    @staticmethod
    def dispatch_key_event(  # noqa: PLR0912
        type: KeyEventType,
        modifiers: Optional[KeyModifier] = None,
        timestamp: Optional[float] = None,
        text: Optional[str] = None,
        unmodified_text: Optional[str] = None,
        key_identifier: Optional[str] = None,
        code: Optional[str] = None,
        key: Optional[str] = None,
        windows_virtual_key_code: Optional[int] = None,
        native_virtual_key_code: Optional[int] = None,
        auto_repeat: Optional[bool] = None,
        is_keypad: Optional[bool] = None,
        is_system_key: Optional[bool] = None,
        location: Optional[KeyLocation] = None,
        commands: Optional[list[str]] = None,
    ) -> DispatchKeyEventCommand:
        """
        Generates a command to dispatch a key event to the page.

        This method can simulate various types of keyboard events such as key presses,
        key releases, and character inputs.

        Args:
            type: Type of the key event. Allowed values: keyDown, keyUp, rawKeyDown, char.
                 - keyDown: Corresponds to a user pressing a key
                 - keyUp: Corresponds to a user releasing a key
                 - rawKeyDown: A physical key press, without the text processing
                 - char: Generates a character without explicit key events
            modifiers: Bit field representing pressed modifier keys. Values:
                      Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).
                      For example, to simulate Ctrl+Shift, use 10.
            timestamp: Time at which the event occurred, in seconds since epoch.
            text: Text as generated by processing a virtual key code with a keyboard layout.
                 Not needed for 'keyUp' and 'rawKeyDown' events (default: "").
            unmodified_text: Text that would have been generated by the keyboard without modifiers
                           (except for shift). Useful for shortcut key handling (default: "").
            key_identifier: Unique key identifier (e.g., 'U+0041') (default: "").
            code: Unique DOM defined string value for each physical key (e.g., 'KeyA')
                (default: "").
            key: Unique DOM defined string value describing the meaning of the key in the
                context of active modifiers, keyboard layout, etc. (e.g., 'AltGr')
                (default: "").
            windows_virtual_key_code: Windows virtual key code (default: 0).
            native_virtual_key_code: Native virtual key code (default: 0).
            auto_repeat: Whether the event was generated from auto repeat (default: false).
            is_keypad: Whether the event was generated from the keypad (default: false).
            is_system_key: Whether the event was a system key event (default: false).
            location: Whether the event was from the left or right side of the keyboard:
                     0=Default, 1=Left, 2=Right (default: 0).
            commands: Editing commands to send with the key event (e.g., 'selectAll')
                     (default: []). These are related to but not equal to the command names
                     used in `document.execCommand` and NSStandardKeyBindingResponding.

        Returns:
            Command: The CDP command to dispatch the key event.
        """
        params = DispatchKeyEventParams(type=type)
        if modifiers is not None:
            params['modifiers'] = modifiers
        if timestamp is not None:
            params['timestamp'] = timestamp
        if text is not None:
            params['text'] = text
        if unmodified_text is not None:
            params['unmodifiedText'] = unmodified_text
        if key_identifier is not None:
            params['keyIdentifier'] = key_identifier
        if code is not None:
            params['code'] = code
        if key is not None:
            params['key'] = key
        if windows_virtual_key_code is not None:
            params['windowsVirtualKeyCode'] = windows_virtual_key_code
        if native_virtual_key_code is not None:
            params['nativeVirtualKeyCode'] = native_virtual_key_code
        if auto_repeat is not None:
            params['autoRepeat'] = auto_repeat
        if is_keypad is not None:
            params['isKeypad'] = is_keypad
        if is_system_key is not None:
            params['isSystemKey'] = is_system_key
        if location is not None:
            params['location'] = location
        if commands is not None:
            params['commands'] = commands
        return Command(method=InputMethod.DISPATCH_KEY_EVENT, params=params)

    @staticmethod
    def dispatch_mouse_event(
        type: MouseEventType,
        x: int,
        y: int,
        modifiers: Optional[KeyModifier] = None,
        timestamp: Optional[float] = None,
        button: Optional[MouseButton] = None,
        click_count: Optional[int] = None,
        force: Optional[float] = None,
        tangential_pressure: Optional[float] = None,
        tilt_x: Optional[float] = None,
        tilt_y: Optional[float] = None,
        twist: Optional[int] = None,
        delta_x: Optional[float] = None,
        delta_y: Optional[float] = None,
        pointer_type: Optional[PointerType] = None,
    ) -> DispatchMouseEventCommand:
        """
        Generates a command to dispatch a mouse event to the page.

        This method allows simulating various mouse interactions such as clicks,
        movements, and wheel scrolling.

        Args:
            type: Type of the mouse event. Allowed values:
                 - mousePressed: Mouse button pressed
                 - mouseReleased: Mouse button released
                 - mouseMoved: Mouse moved
                 - mouseWheel: Mouse wheel rotated
            x: X coordinate of the event relative to the main frame's viewport in CSS pixels.
            y: Y coordinate of the event relative to the main frame's viewport in CSS pixels.
                0 refers to the top of the viewport, and Y increases going down.
            modifiers: Bit field representing pressed modifier keys. Values:
                Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).
            timestamp: Time at which the event occurred, in seconds since epoch.
            button: Mouse button being pressed/released. Default is "none".
                Allowed values: "none", "left", "middle", "right", "back", "forward".
            click_count: Number of times the mouse button was clicked (default: 0).
                For example, 2 for a double-click.
            force: The normalized pressure, which has a range of [0,1] (default: 0).
                Used primarily for pressure-sensitive inputs.
            tangential_pressure: The normalized tangential pressure, which has a range
                of [-1,1] (default: 0). Used for stylus input.
            tilt_x: The plane angle between the Y-Z plane and the plane containing both the stylus
                axis and the Y axis, in degrees of the range [-90,90]. A positive tiltX is
                to the right (default: 0).
            tilt_y: The plane angle between the X-Z plane and the plane containing both the stylus
                axis and the X axis, in degrees of the range [-90,90]. A positive tiltY is
                towards the user (default: 0).
            twist: The clockwise rotation of a pen stylus around its own major axis,
                in degrees in the range [0,359] (default: 0).
            delta_x: X delta in CSS pixels for mouse wheel event (default: 0).
                Positive values scroll right.
            delta_y: Y delta in CSS pixels for mouse wheel event (default: 0).
                Positive values scroll up.
            pointer_type: Pointer type (default: "mouse"). Allowed values: "mouse", "pen".

        Returns:
            Command: The CDP command to dispatch the mouse event.
        """
        params = DispatchMouseEventParams(type=type, x=x, y=y)
        if modifiers is not None:
            params['modifiers'] = modifiers
        if timestamp is not None:
            params['timestamp'] = timestamp
        if button is not None:
            params['button'] = button
        if click_count is not None:
            params['clickCount'] = click_count
        if force is not None:
            params['force'] = force
        if tangential_pressure is not None:
            params['tangentialPressure'] = tangential_pressure
        if tilt_x is not None:
            params['tiltX'] = tilt_x
        if tilt_y is not None:
            params['tiltY'] = tilt_y
        if twist is not None:
            params['twist'] = twist
        if delta_x is not None:
            params['deltaX'] = delta_x
        if delta_y is not None:
            params['deltaY'] = delta_y
        if pointer_type is not None:
            params['pointerType'] = pointer_type
        return Command(method=InputMethod.DISPATCH_MOUSE_EVENT, params=params)

    @staticmethod
    def dispatch_touch_event(
        type: TouchEventType,
        touch_points: list[TouchPoint],
        modifiers: Optional[KeyModifier] = None,
        timestamp: Optional[float] = None,
    ) -> DispatchTouchEventCommand:
        """
        Generates a command to dispatch a touch event to the page.

        This method allows simulating touch interactions on touch-enabled devices
        or emulated touch environments.

        Args:
            type: Type of the touch event. Allowed values:
                 - touchStart: Touch started - at least one point must be specified
                 - touchEnd: Touch ended - points that are no longer pressed should be removed
                 - touchMove: Touch moved - active points should be updated
                 - touchCancel: Touch canceled - clears all touch points
                 Touch end and cancel events must not contain any touch points,
                 while touch start and move must contain at least one.
            touch_points: list of active touch points. One event per any changed point
                        (compared to previous event) is generated, emulating
                        pressing/moving/releasing points one by one.
                        Each point includes coordinates and other properties.
            modifiers: Bit field representing pressed modifier keys. Values:
                      Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).
            timestamp: Time at which the event occurred, in seconds since epoch.

        Returns:
            Command: The CDP command to dispatch the touch event.
        """
        params = DispatchTouchEventParams(type=type, touchPoints=touch_points)
        if modifiers is not None:
            params['modifiers'] = modifiers
        if timestamp is not None:
            params['timestamp'] = timestamp
        return Command(method=InputMethod.DISPATCH_TOUCH_EVENT, params=params)

    @staticmethod
    def set_ignore_input_events(ignore: bool) -> SetIgnoreInputEventsCommand:
        """
        Generates a command to ignore input events (useful while auditing page).

        When ignore is true, all input events will be ignored, which can be useful
        during automated tests or when you want to prevent user interaction
        while performing certain operations.

        Args:
            ignore: If true, input events processing will be ignored.

        Returns:
            Command: The CDP command to set ignore input events.
        """
        params = SetIgnoreInputEventsParams(ignore=ignore)
        return Command(method=InputMethod.SET_IGNORE_INPUT_EVENTS, params=params)

    @staticmethod
    def dispatch_drag_event(
        type: DragEventType,
        x: int,
        y: int,
        data: DragData,
        modifiers: Optional[KeyModifier] = None,
    ) -> DispatchDragEventCommand:
        """
        Generates a command to dispatch a drag event into the page.

        This experimental method allows simulating drag and drop operations
        by dispatching drag events at specific coordinates.

        Args:
            type: Type of the drag event. Allowed values:
                 - dragEnter: Fired when a dragged item enters a valid drop target
                 - dragOver: Fired when a dragged item is being dragged over a valid drop target
                 - drop: Fired when an item is dropped on a valid drop target
                 - dragCancel: Fired when a drag operation is being canceled
            x: X coordinate of the event relative to the main frame's viewport in CSS pixels.
            y: Y coordinate of the event relative to the main frame's viewport in CSS pixels.
                0 refers to the top of the viewport, and Y increases going down.
            data: Drag data containing items being dragged, their MIME types, and other information.
            modifiers: Bit field representing pressed modifier keys. Values:
                      Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).

        Returns:
            Command: The CDP command to dispatch the drag event.
        """
        params = DispatchDragEventParams(type=type, data=data, x=x, y=y)
        if modifiers is not None:
            params['modifiers'] = modifiers
        return Command(method=InputMethod.DISPATCH_DRAG_EVENT, params=params)

    @staticmethod
    def emulate_touch_from_mouse_event(  # noqa: PLR0913, PLR0917
        type: MouseEventType,
        x: int,
        y: int,
        button: MouseButton,
        timestamp: Optional[float] = None,
        delta_x: Optional[float] = None,
        delta_y: Optional[float] = None,
        modifiers: Optional[KeyModifier] = None,
        click_count: Optional[int] = None,
    ) -> EmulateTouchFromMouseEventCommand:
        """
        Generates a command to emulate touch event from the mouse event parameters.

        This experimental method allows converting mouse events into touch events,
        useful for testing touch interactions in environments where touch is not available.

        Args:
            type: Type of the mouse event to convert. Allowed values:
                 - mousePressed: Converted to touchStart
                 - mouseReleased: Converted to touchEnd
                 - mouseMoved: Converted to touchMove
                 - mouseWheel: May trigger scrolling
            x: X coordinate of the mouse pointer in device-independent pixels (DIP).
            y: Y coordinate of the mouse pointer in DIP.
            button: Mouse button. Only "none", "left", "right" are supported.
            timestamp: Time at which the event occurred, in seconds since epoch.
                      Default is current time.
            delta_x: X delta in DIP for mouse wheel event (default: 0). Used for scrolling.
            delta_y: Y delta in DIP for mouse wheel event (default: 0). Used for scrolling.
            modifiers: Bit field representing pressed modifier keys. Values:
                      Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).
            click_count: Number of times the mouse button was clicked (default: 0).
                       For example, 2 for a double-click.

        Returns:
            Command: The CDP command to emulate touch from mouse event.
        """
        params = EmulateTouchFromMouseEventParams(type=type, x=x, y=y, button=button)
        if timestamp is not None:
            params['timestamp'] = timestamp
        if delta_x is not None:
            params['deltaX'] = delta_x
        if delta_y is not None:
            params['deltaY'] = delta_y
        if modifiers is not None:
            params['modifiers'] = modifiers
        if click_count is not None:
            params['clickCount'] = click_count
        return Command(method=InputMethod.EMULATE_TOUCH_FROM_MOUSE_EVENT, params=params)

    @staticmethod
    def ime_set_composition(
        text: str,
        selection_start: int,
        selection_end: int,
        replacement_start: Optional[int] = None,
        replacement_end: Optional[int] = None,
    ) -> ImeSetCompositionCommand:
        """
        Generates a command to set the current candidate text for IME.

        This experimental method sets the text for Input Method Editors (IME),
        which are used for entering characters in languages that require more
        keystrokes than the number of characters (like Chinese, Japanese, Korean).

        Use imeCommitComposition to commit the final text.
        Use imeSetComposition with empty string as text to cancel composition.

        Args:
            text: The text to insert as the IME composition.
            selection_start: Start position of the selection within the composition text.
            selection_end: End position of the selection within the composition text.
            replacement_start: Start position of the text to be replaced
                (default: same as selection_start).
            replacement_end: End position of the text to be replaced
                (default: same as selection_end).

        Returns:
            Command: The CDP command to set IME composition.
        """
        params = ImeSetCompositionParams(
            text=text,
            selectionStart=selection_start,
            selectionEnd=selection_end,
        )
        if replacement_start is not None:
            params['replacementStart'] = replacement_start
        if replacement_end is not None:
            params['replacementEnd'] = replacement_end
        return Command(method=InputMethod.IME_SET_COMPOSITION, params=params)

    @staticmethod
    def insert_text(
        text: str,
    ) -> InsertTextCommand:
        """
        Generates a command to emulate inserting text that doesn't come from a key press.

        This experimental method is useful for inserting text that would normally
        come from sources other than keyboard, such as emoji pickers, IMEs, or
        clipboard pastes.

        Args:
            text: The text to insert.

        Returns:
            Command: The CDP command to insert text.
        """
        params = InsertTextParams(text=text)
        return Command(method=InputMethod.INSERT_TEXT, params=params)

    @staticmethod
    def set_intercept_drags(enabled: bool) -> SetInterceptDragsCommand:
        """
        Generates a command to control interception of drag and drop events.

        This experimental method prevents default drag and drop behavior and instead
        emits Input.dragIntercepted events. Drag and drop behavior can then be
        directly controlled via Input.dispatchDragEvent.

        This is useful for implementing custom drag and drop logic or for testing
        drag and drop behavior in automated tests.

        Args:
            enabled: If true, drag events will be intercepted and reported as
                    dragIntercepted events, preventing the default behavior.

        Returns:
            Command: The CDP command to set drag interception.
        """
        params = SetInterceptDragsParams(enabled=enabled)
        return Command(method=InputMethod.SET_INTERCEPT_DRAGS, params=params)

    @staticmethod
    def synthesize_pinch_gesture(
        x: int,
        y: int,
        scale_factor: float,
        relative_speed: Optional[int] = None,
        gesture_source_type: Optional[GestureSourceType] = None,
    ) -> SynthesizePinchGestureCommand:
        """
        Generates a command to synthesize a pinch gesture over a time period.

        This experimental method creates a synthetic pinch gesture (zoom in/out)
        by issuing appropriate touch events over time. This is useful for testing
        pinch-to-zoom functionality in web applications.

        Args:
            x: X coordinate of the start of the gesture in CSS pixels.
            y: Y coordinate of the start of the gesture in CSS pixels.
            scale_factor: Relative scale factor after zooming:
                        - >1.0 zooms in (fingers moving apart)
                        - <1.0 zooms out (fingers moving together)
            relative_speed: Relative pointer speed in pixels per second (default: 800).
                          Controls how fast the gesture happens.
            gesture_source_type: Which type of input events to be generated:
                              - 'default': Platform's preferred input type
                              - 'touch': Touch input
                              - 'mouse': Mouse input

        Returns:
            Command: The CDP command to synthesize a pinch gesture.
        """
        params = SynthesizePinchGestureParams(x=x, y=y, scaleFactor=scale_factor)
        if relative_speed is not None:
            params['relativeSpeed'] = relative_speed
        if gesture_source_type is not None:
            params['gestureSourceType'] = gesture_source_type
        return Command(method=InputMethod.SYNTHESIZE_PINCH_GESTURE, params=params)

    @staticmethod
    def synthesize_scroll_gesture(
        x: int,
        y: int,
        x_distance: Optional[float] = None,
        y_distance: Optional[float] = None,
        x_overscroll: Optional[float] = None,
        y_overscroll: Optional[float] = None,
        prevent_fling: Optional[bool] = None,
        speed: Optional[int] = None,
        gesture_source_type: Optional[GestureSourceType] = None,
        repeat_count: Optional[int] = None,
        repeat_delay_ms: Optional[int] = None,
        interaction_marker_name: Optional[str] = None,
    ) -> SynthesizeScrollGestureCommand:
        """
        Generates a command to synthesize a scroll gesture over a time period.

        This experimental method creates a synthetic scroll gesture by issuing
        appropriate touch events over time. This is useful for testing scrolling
        behavior in web applications.

        Args:
            x: X coordinate of the start of the gesture in CSS pixels.
            y: Y coordinate of the start of the gesture in CSS pixels.
            x_distance: The distance to scroll along the X axis (positive to scroll left).
            y_distance: The distance to scroll along the Y axis (positive to scroll up).
            x_overscroll: The number of additional pixels to scroll back along the X axis,
                        in addition to the given distance. This creates an overscroll
                        effect (rubber-banding).
            y_overscroll: The number of additional pixels to scroll back along the Y axis,
                        in addition to the given distance. This creates an overscroll
                        effect (rubber-banding).
            prevent_fling: Prevent fling (default: true). If false, a fling animation might
                         continue after the gesture.
            speed: Swipe speed in pixels per second (default: 800).
            gesture_source_type: Which type of input events to be generated:
                              - 'default': Platform's preferred input type
                              - 'touch': Touch input
                              - 'mouse': Mouse input
            repeat_count: The number of times to repeat the gesture (default: 0).
            repeat_delay_ms: The number of milliseconds delay between each repeat (default: 250).
            interaction_marker_name: The name of the interaction markers to generate, if not empty.
                                  Used for tracking gesture timing in performance measurements.

        Returns:
            Command: The CDP command to synthesize a scroll gesture.
        """
        params = SynthesizeScrollGestureParams(x=x, y=y)
        if x_distance is not None:
            params['xDistance'] = x_distance
        if y_distance is not None:
            params['yDistance'] = y_distance
        if x_overscroll is not None:
            params['xOverscroll'] = x_overscroll
        if y_overscroll is not None:
            params['yOverscroll'] = y_overscroll
        if prevent_fling is not None:
            params['preventFling'] = prevent_fling
        if speed is not None:
            params['speed'] = speed
        if gesture_source_type is not None:
            params['gestureSourceType'] = gesture_source_type
        if repeat_count is not None:
            params['repeatCount'] = repeat_count
        if repeat_delay_ms is not None:
            params['repeatDelayMs'] = repeat_delay_ms
        if interaction_marker_name is not None:
            params['interactionMarkerName'] = interaction_marker_name
        return Command(method=InputMethod.SYNTHESIZE_SCROLL_GESTURE, params=params)

    @staticmethod
    def synthesize_tap_gesture(
        x: int,
        y: int,
        duration: Optional[int] = None,
        tap_count: Optional[int] = None,
        gesture_source_type: Optional[GestureSourceType] = None,
    ) -> SynthesizeTapGestureCommand:
        """
        Generates a command to synthesize a tap gesture over a time period.

        This experimental method creates a synthetic tap gesture by issuing
        appropriate touch events over time. This is useful for testing
        touch interaction in web applications.

        Args:
            x: X coordinate of the start of the gesture in CSS pixels.
            y: Y coordinate of the start of the gesture in CSS pixels.
            duration: Duration between touchdown and touchup events in milliseconds (default: 50).
                     Controls how long the tap gesture takes.
            tap_count: Number of times to perform the tap (e.g., 2 for a double tap, default: 1).
            gesture_source_type: Which type of input events to be generated:
                              - 'default': Platform's preferred input type
                              - 'touch': Touch input
                              - 'mouse': Mouse input

        Returns:
            Command: The CDP command to synthesize a tap gesture.
        """
        params = SynthesizeTapGestureParams(x=x, y=y)
        if duration is not None:
            params['duration'] = duration
        if tap_count is not None:
            params['tapCount'] = tap_count
        if gesture_source_type is not None:
            params['gestureSourceType'] = gesture_source_type
        return Command(method=InputMethod.SYNTHESIZE_TAP_GESTURE, params=params)
